package com.sishu.njrtsdms.service.impl;


import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.io.FileTypeUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sishu.njrtsdms.config.NjrtsDMSConfig;
import com.sishu.njrtsdms.domain.core.GenericServiceImpl;
import com.sishu.njrtsdms.domain.core.PageInfo;
import com.sishu.njrtsdms.domain.dto.TcpPointAssignRecordDTO;
import com.sishu.njrtsdms.domain.dto.TeachConPrjAuditDTO;
import com.sishu.njrtsdms.domain.dto.TeachConPrjDTO;
import com.sishu.njrtsdms.domain.vo.*;
import com.sishu.njrtsdms.enums.DictDataEnum;
import com.sishu.njrtsdms.exception.ServiceException;
import com.sishu.njrtsdms.jooq.codegen.tables.daos.*;
import com.sishu.njrtsdms.jooq.codegen.tables.pojos.SysFile;
import com.sishu.njrtsdms.jooq.codegen.tables.pojos.TcpAuditRecord;
import com.sishu.njrtsdms.jooq.codegen.tables.pojos.TeachConPrj;
import com.sishu.njrtsdms.jooq.codegen.tables.pojos.TeachConPrjProofFile;
import com.sishu.njrtsdms.jooq.codegen.tables.records.TcpPointAssignRecordRecord;
import com.sishu.njrtsdms.manager.DictionaryManager;
import com.sishu.njrtsdms.service.SysFileService;
import com.sishu.njrtsdms.service.TeachConPrjService;
import com.sishu.njrtsdms.utils.SecurityUtils;
import com.sishu.njrtsdms.utils.StringUtils;
import com.sishu.njrtsdms.utils.file.FileUploadUtils;
import com.sishu.njrtsdms.utils.file.FileUtils;
import com.sishu.njrtsdms.utils.uuid.IdUtils;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.hssf.usermodel.*;
import org.apache.poi.hssf.util.HSSFColor;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.util.CellRangeAddress;
import org.jooq.DSLContext;
import org.jooq.JSON;
import org.jooq.tools.json.JSONArray;
import org.jooq.tools.json.JSONObject;
import org.jooq.types.ULong;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

import static com.sishu.njrtsdms.jooq.codegen.Tables.*;

@Service
@Slf4j
public class TeachConPrjServiceImpl extends GenericServiceImpl<TeachConPrj, org.jooq.types.ULong>
        implements TeachConPrjService {

    private final TeachConPrjDAO teachConPrjDAO;
    private final DictionaryManager dictionaryManager;
    private final SysFileDAO sysFileDAO;
    private final TeachConPrjProofFileDAO teachConPrjProofFileDAO;
    private final TcpAuditRecordDAO tcpAuditRecordDAO;
    private final TcpPointAssignRecordDAO tcpPointAssignRecordDAO;
    private final DSLContext dslContext;
    private final SysFileService sysFileService;
    private final TcpDeclareBelongYearConfigDAO belongYearConfigDAO;

    private final ObjectMapper objectMapper;

    private final CollegeDAO collegeDAO;

    private final UserDAO userDAO;


    public TeachConPrjServiceImpl(TeachConPrjDAO teachConPrjDAO,
                                  DictionaryManager dictionaryManager,
                                  SysFileDAO sysFileDAO,
                                  TeachConPrjProofFileDAO teachConPrjProofFileDAO,
                                  TcpAuditRecordDAO tcpAuditRecordDAO,
                                  TcpPointAssignRecordDAO tcpPointAssignRecordDAO,
                                  DSLContext dslContext,
                                  SysFileService sysFileService,
                                  TcpDeclareBelongYearConfigDAO belongYearConfigDAO,
                                  ObjectMapper objectMapper,
                                  CollegeDAO collegeDAO,
                                  UserDAO userDAO) {
        super(teachConPrjDAO);
        this.teachConPrjDAO = teachConPrjDAO;
        this.dictionaryManager = dictionaryManager;
        this.sysFileDAO = sysFileDAO;
        this.teachConPrjProofFileDAO = teachConPrjProofFileDAO;
        this.tcpAuditRecordDAO = tcpAuditRecordDAO;
        this.tcpPointAssignRecordDAO = tcpPointAssignRecordDAO;
        this.dslContext = dslContext;
        this.sysFileService = sysFileService;
        this.belongYearConfigDAO = belongYearConfigDAO;
        this.objectMapper = objectMapper;
        this.collegeDAO = collegeDAO;
        this.userDAO = userDAO;
    }

    @Override
    public PageInfo<TeachConPrjVO> page(TeachConPrjDTO teachConPrjDTO) {
        return teachConPrjDAO.page(teachConPrjDTO);
    }

    @Override
    public TeachConPrjVO queryOneTeachConPrj(Long tcpId) {
        return teachConPrjDAO.fetchOneTeachConPrjVOByTCPId(ULong.valueOf(tcpId));
    }

    @Override
    public Long totalCount(TeachConPrjDTO teachConPrjDTO) {
        return teachConPrjDAO.totalCount(teachConPrjDTO);
    }

    @Override
    public void create(TeachConPrjDTO teachConPrjDTO) {
        // 对应的操作是"创建",执行完成后项目状态为待申报
        TeachConPrj teachConPrj = fromDtoToPojo(null, teachConPrjDTO, false);
        // 如果没有设置负责人,则默认负责人为当前创建的用户即申报人
        if (teachConPrj.getProjectLeaders() == null) {
            teachConPrj.setProjectLeaders(
                    JSON.json(JSONArray.toJSONString(Collections.singletonList(SecurityUtils.getUserId()))));
        }
        teachConPrjDAO.insertSelective(teachConPrj);

    }

    @Override
    public void updateProject(ULong tcpId, TeachConPrjDTO teachConPrjDTO) {
        TeachConPrj teachConPrj = teachConPrjDAO.fetchOneByTcpId(tcpId);
        if (teachConPrj == null || teachConPrj.getIsDeleted())
            throw new ServiceException("项目不存在或已被删除");
        ULong toBeDeclaredDictId = dictionaryManager.getIdByEnum(DictDataEnum.TO_BE_DECLARED);
        ULong sendBackDictId = dictionaryManager.getIdByEnum(DictDataEnum.SEND_BACK);
        // 只允许修改待申报或退回的项目
        boolean isToBeDeclared = teachConPrj.getProjectAuditStatusDict().equals(toBeDeclaredDictId);
        boolean isSendBack = teachConPrj.getProjectAuditStatusDict().equals(sendBackDictId);
        if (!isToBeDeclared && !isSendBack)
            throw new ServiceException("只有待申报或退回的项目允许修改");
        // 如果是修改退回项目,那么需要将申报日期时间置空
        TeachConPrj teachConPrj4Update = fromDtoToPojo(teachConPrj, teachConPrjDTO, isSendBack);
        teachConPrj4Update.setTcpId(tcpId);
        teachConPrjDAO.update(teachConPrj4Update);
    }

    @Override
    public void deleteProject(ULong tcpId) {
        // 允许删除所有状态的项目,除了待申报是物理删除,其他状态都是逻辑删除
        TeachConPrj teachConPrj = teachConPrjDAO.fetchOneByTcpId(tcpId);
        if (teachConPrj == null || teachConPrj.getIsDeleted())
            throw new ServiceException("项目不存在或已被删除");
        ULong toBeDeclaredDictId = dictionaryManager.getIdByEnum(DictDataEnum.TO_BE_DECLARED);
        ULong projectAuditStatusDict = teachConPrj.getProjectAuditStatusDict();
        if (projectAuditStatusDict.equals(toBeDeclaredDictId)) {
            completelyDeleteProject(tcpId);
        } else {// 退回或审核不通过逻辑删除,标记项目为已删除状态
            TeachConPrj teachConPrj4Delete = new TeachConPrj();
            teachConPrj4Delete.setTcpId(tcpId);
            teachConPrj4Delete.setIsDeleted(Boolean.TRUE);
            teachConPrjDAO.updateSelective(teachConPrj4Delete);
        }


    }

    /**
     * 完全删除项目
     *
     * @param tcpId 项目id
     */
    private void completelyDeleteProject(ULong tcpId) {
        // 1.删除项目审核过程中产生的记录信息
        // 1.1 删除绩点分配记录
        dslContext.deleteFrom(TBL_TCP_POINT_ASSIGN_RECORD)
                .where(TBL_TCP_POINT_ASSIGN_RECORD.TCP_ID.eq(tcpId))
                .execute();
        // 1.2 删除审核记录
        dslContext.deleteFrom(TBL_TCP_AUDIT_RECORD)
                .where(TBL_TCP_AUDIT_RECORD.TCP_ID.eq(tcpId))
                .execute();
        // 2.删除项目关联的佐证文件
        var projectProofFiles = listProjectProofFiles(tcpId);
        if (CollUtil.isNotEmpty(projectProofFiles)) {
            var projectProofFileUuids = projectProofFiles
                    .stream()
                    .map(SysFileVO::uuid)
                    .toList();
            sysFileService.deleteFilesByUUIDs(projectProofFileUuids);
        }
        // 3.删除项目本身信息数据
        teachConPrjDAO.deleteById(tcpId);
    }

    @Override
    public void declare(ULong tcpId) {
        var now = LocalDateTime.now();
        // 查出此刻属于哪个申报年度时间段中
        var tcpDeclareBelongYearConfigRecords = dslContext
                .selectFrom(TBL_TCP_DECLARE_BELONG_YEAR_CONFIG)
                .orderBy(TBL_TCP_DECLARE_BELONG_YEAR_CONFIG.BELONG_YEAR.desc())
                .fetch();

        var belongYearConfigRecord = tcpDeclareBelongYearConfigRecords.stream()
                .filter(it -> {
                    var declareBeginDateTime = it.getDeclareBeginDateTime();
                    var declareEndDateTime = it.getDeclareEndDateTime();
                    return LocalDateTimeUtil.isIn(now, declareBeginDateTime, declareEndDateTime);
                })
                .findFirst()
                .orElseThrow(() -> new ServiceException("当前时间不属于任何可申报期内,不允许申报,具体情况请联系管理员"));

        var teachConPrj = teachConPrjDAO.fetchOneByTcpId(tcpId);
        if (teachConPrj == null || teachConPrj.getIsDeleted())
            throw new ServiceException("项目不存在或已被删除");
        var toBeDeclaredDictId = dictionaryManager.getIdByEnum(DictDataEnum.TO_BE_DECLARED);
        var sendBackDictId = dictionaryManager.getIdByEnum(DictDataEnum.SEND_BACK);
        // 只能申报未提交或退回的项目
        if (!teachConPrj.getProjectAuditStatusDict().equals(toBeDeclaredDictId)
                && !teachConPrj.getProjectAuditStatusDict().equals(sendBackDictId)) {
            throw new ServiceException("只有未提交或退回的项目允许申报");
        }
        // 修改为部门审核中状态,并更新申报日期时间和申报人信息
        var teachConPrjForDeclare = new TeachConPrj();
        teachConPrjForDeclare.setTcpId(tcpId);
        // 先查询该部门是否有院长，如果没有院长直接变为管理员审核中
        ULong tcpCollegeId = teachConPrj.getCollegeId();
        long deanCount = dslContext.selectFrom(TBL_USER)
                .where(TBL_USER.COLLEGE_ID.eq(tcpCollegeId))
                .and(TBL_USER.USER_TYPE_DICT.eq(dictionaryManager.getIdByEnum(DictDataEnum.DEAN)))
                .stream()
                .count();
        log.info(StrUtil.format("collegeId为:[{}]的学院有:[{}]个院长"), deanCount);
        if (deanCount > 0)
            teachConPrjForDeclare.setProjectAuditStatusDict(
                    dictionaryManager.getIdByEnum(DictDataEnum.DEPT_AUDITING)
            );
        else
            teachConPrjForDeclare.setProjectAuditStatusDict(
                    dictionaryManager.getIdByEnum(DictDataEnum.ADMIN_AUDITING)
            );

        teachConPrjForDeclare.setDeclareDateTime(now);
        teachConPrjForDeclare.setDeclarer(SecurityUtils.getUserId());
        teachConPrjForDeclare.setTcpDbycId(belongYearConfigRecord.getTcpDbycId());
        teachConPrjDAO.updateSelective(teachConPrjForDeclare);
    }

    @Override
    public void uploadProjectFiles(ULong tcpId, List<MultipartFile> files) {
        files.forEach(file -> {
            String filename = file.getOriginalFilename();
            String fileType = StrUtil.subAfter(filename, ".", true);
            try (InputStream stream = file.getInputStream()) {
                //先判断后缀名
                //根据首部字节判断文件类型
                String[] imageTypeStrArr = new String[]{"jpg", "jpeg", "bmp", "gif", "png"};
                String type;
                type = FileTypeUtil.getType(stream);
                if (!StrUtil.equalsAny(fileType, imageTypeStrArr)
                        ||
                        !StrUtil.equalsAny(type, imageTypeStrArr)
                ) {
                    throw new ServiceException(StrUtil.format(
                            "作证材料只允许上传图片,包括以下类型:[{}],[{}],[{}],[{}],[{}]",
                            (Object[]) imageTypeStrArr
                    ));
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        });

        // 校验项目是否存在
        var teachConPrj = teachConPrjDAO.fetchOneByTcpId(tcpId);
        if (teachConPrj == null || teachConPrj.getIsDeleted())
            throw new RuntimeException("申报项目不存在或已被删除");
        DictDataEnum projectAuditStatusDictEnum = dictionaryManager
                .getEnumById(teachConPrj.getProjectAuditStatusDict());
        if (
                !(projectAuditStatusDictEnum.equals(DictDataEnum.SEND_BACK)
                        ||
                        projectAuditStatusDictEnum.equals(DictDataEnum.TO_BE_DECLARED))
        ) {
            throw new ServiceException(StrUtil.format(
                    "只有项目处于[{}]或[{}]状态时允许上传佐证材料",
                    DictDataEnum.SEND_BACK.getDictDataLabel(),
                    DictDataEnum.TO_BE_DECLARED.getDictDataLabel()
            ));
        }
        // 上传文件
        try {
            // 上传文件路径前缀
            var filePathPrefix = NjrtsDMSConfig.getUploadPath();
            // 文件信息存储
            for (MultipartFile file : files) {
                // 上传并返回新文件名称
                var fileName = FileUploadUtils.upload(filePathPrefix, file);
                // 上传文件路径后缀
                var filePathSuffix = StringUtils.substringAfter(fileName, "upload");
                // 文件最终存储盘符路径
                var drivePath = filePathPrefix + filePathSuffix;
                var sysFile = new SysFile();
                var uuid = IdUtils.simpleUUID();
                sysFile.setUuid(uuid);
                sysFile.setOriginalName(file.getOriginalFilename());
                sysFile.setFileName(FileUtils.getName(fileName));
                sysFile.setSuffix(FileUploadUtils.getExtension(file));
                sysFile.setDrivePath(drivePath);
                sysFile.setUploadDateTime(LocalDateTime.now());
                sysFile.setUploadBy(SecurityUtils.getUserId());
                sysFileDAO.insertSelective(sysFile);
                // 预期目标佐证材料记录
                var projectProofFile = new TeachConPrjProofFile();
                projectProofFile.setUuid(uuid);
                projectProofFile.setTcpId(tcpId);
                teachConPrjProofFileDAO.insertSelective(projectProofFile);
            }
        } catch (Exception e) {
            throw new ServiceException(e.getMessage());
        }
    }

    @Override
    public List<SysFileVO> listProjectProofFiles(ULong tcpId) {
        return sysFileDAO.listProjectProofFilesByTcpId(tcpId);
    }

    @Override
    public void auditProject(ULong tcpId, TeachConPrjAuditDTO teachConPrjAuditDTO) {
        var teachConPrj = teachConPrjDAO.fetchOneByTcpId(tcpId);
        if (teachConPrj == null || teachConPrj.getIsDeleted())
            throw new RuntimeException("项目不存在或已被终止");
        var projectAuditStatusDict = teachConPrj.getProjectAuditStatusDict();
        var projectAuditStatusEnum = dictionaryManager.getEnumById(projectAuditStatusDict);
        if (!Objects.equals(projectAuditStatusDict, dictionaryManager.getIdByEnum(DictDataEnum.DEPT_AUDITING)) &&
                !Objects.equals(projectAuditStatusDict, dictionaryManager.getIdByEnum(DictDataEnum.ADMIN_AUDITING))) {
            throw new RuntimeException(StrUtil.format(
                    "项目只有在部门审核中或管理员审核中状态下可以进行审核,当前为[{}] 状态",
                    projectAuditStatusEnum.getDictDataLabel()
            ));
        }
        var currentUser = SecurityUtils.getLoginUserOptional()
                .orElseThrow(() -> new ServiceException("获取用户信息异常"))
                .getUser();
        var currentUserUserTypeDictDataId = currentUser.userTypeDict().dictDataId();
        var currentUserUserTypeEnum = dictionaryManager.getEnumById(currentUserUserTypeDictDataId);
        // 教师不允许审核
        if (currentUserUserTypeEnum.equals(DictDataEnum.TEACHER))
            throw new RuntimeException("只有院长或管理员允许审核项目");

        // 新增审核记录
        var tcpAuditRecord = new TcpAuditRecord();
        tcpAuditRecord.setTcpId(tcpId);
        tcpAuditRecord.setAuditor(currentUser.userId());
        ULong auditLevelDict;
        DictDataEnum auditLevelEnum;
        var auditOperDictEnum = dictionaryManager.getEnumById(teachConPrjAuditDTO.auditOperDictId());
        var auditProjectGpaBonusMark = teachConPrjAuditDTO.auditProjectGpaBonusMark();
        switch (currentUserUserTypeEnum) {
            case DEAN -> auditLevelEnum = DictDataEnum.DEPT_AUDIT_LEVEL;
            case ADMIN -> {
                // 校验管理员审核通过时绩点评分是否为空
                if (auditOperDictEnum.equals(DictDataEnum.AUDIT_PASS_OP)
                        && auditProjectGpaBonusMark == null) {
                    throw new ServiceException("管理员审核通过时必须给出最终绩点评分");
                }
                auditLevelEnum = DictDataEnum.ADMIN_AUDIT_LEVEL;
            }
            default -> throw new ServiceException("内部异常");
        }
        auditLevelDict = dictionaryManager.getIdByEnum(auditLevelEnum);
        if (auditLevelDict == null) {
            log.error("严重错误!字典管理器未获取到对应枚举ID,对应枚举名为:{}", auditLevelEnum);
            throw new ServiceException("内部异常");
        }

        // 更新项目审核状态
        var teachConPrjForUpdate = new TeachConPrj();
        teachConPrjForUpdate.setTcpId(tcpId);
        ULong projectNewAuditStatusDict = null;
        switch (currentUserUserTypeEnum) {
            case DEAN -> {
                if (projectAuditStatusEnum.equals(DictDataEnum.ADMIN_AUDITING))
                    throw new ServiceException(
                            StrUtil.format("项目当前为[{}]状态,非管理员不允许审核",
                                    currentUserUserTypeEnum.getDictDataLabel()
                            )
                    );
                switch (auditOperDictEnum) {
                    case AUDIT_PASS_OP -> projectNewAuditStatusDict = dictionaryManager
                            .getIdByEnum(DictDataEnum.ADMIN_AUDITING);
                    case SEND_BACK_OP -> projectNewAuditStatusDict = dictionaryManager
                            .getIdByEnum(DictDataEnum.SEND_BACK);
                    case TERMINATE_OP -> throw new ServiceException("只有管理员可以终止项目");
                    default -> throw new ServiceException("未知的审核操作行为?");
                }
            }
            // 管理员审核时,若通过则必须给最终审核绩点评分,并且更新项目的可分配总绩点
            case ADMIN -> {
                switch (auditOperDictEnum) {
                    case AUDIT_PASS_OP -> {
                        projectNewAuditStatusDict = dictionaryManager
                                .getIdByEnum(DictDataEnum.AUDIT_APPROVED);
                        teachConPrjForUpdate
                                .setAssignableTotalGradePoints(auditProjectGpaBonusMark);
                    }
                    case SEND_BACK_OP -> projectNewAuditStatusDict = dictionaryManager
                            .getIdByEnum(DictDataEnum.SEND_BACK);
                    case TERMINATE_OP -> projectNewAuditStatusDict = dictionaryManager
                            .getIdByEnum(DictDataEnum.AUDIT_FAILED);
                    default -> throw new ServiceException("未知的审核操作行为?");
                }
            }
        }
        teachConPrjForUpdate.setProjectAuditStatusDict(projectNewAuditStatusDict);
        teachConPrjDAO.updateSelective(teachConPrjForUpdate);

        tcpAuditRecord.setAuditLevelDict(auditLevelDict);
        tcpAuditRecord.setAuditProjectLevelDict(teachConPrjAuditDTO.auditProjectLevelDictId());
        tcpAuditRecord.setAuditProjectPrizeOrderDict(teachConPrjAuditDTO.auditProjectPrizeOrderDictId());
        tcpAuditRecord.setAuditProjectGpaBonusMark(auditProjectGpaBonusMark);
        tcpAuditRecord.setAuditOpinion(teachConPrjAuditDTO.auditOpinion());
        tcpAuditRecord.setAuditDateTime(LocalDateTime.now());
        tcpAuditRecord.setAuditOperDict(teachConPrjAuditDTO.auditOperDictId());
        tcpAuditRecord.setRemark(teachConPrjAuditDTO.remark());
        tcpAuditRecordDAO.insertSelective(tcpAuditRecord);
    }

    @Override
    public void assignPoint(ULong tcpId, List<TcpPointAssignRecordDTO> tcpPointAssignRecordDTOS) {
        var teachConPrjVO = teachConPrjDAO.fetchOneTeachConPrjVOByTCPId(tcpId);
        if (teachConPrjVO == null || teachConPrjVO.isDeleted())
            throw new RuntimeException("项目不存在或已被终止");
        var projectAuditStatusDict = teachConPrjVO.projectAuditStatusDict().dictDataId();
        var projectAuditStatusEnum = dictionaryManager.getEnumById(projectAuditStatusDict);
        if (!projectAuditStatusEnum.equals(DictDataEnum.AUDIT_APPROVED))
            throw new RuntimeException(StrUtil.format(
                    "项目处于非[{}]状态,不允许进行绩点分配",
                    DictDataEnum.AUDIT_APPROVED.getDictDataLabel()
            ));
        var gpaAssignableBeginDateTime = teachConPrjVO.belongYearConfigInfo().gpaAssignableBeginDateTime();
        var gpaAssignableEndDateTime = teachConPrjVO.belongYearConfigInfo().gpaAssignableEndDateTime();
        if (!LocalDateTimeUtil.isIn(LocalDateTime.now(), gpaAssignableBeginDateTime, gpaAssignableEndDateTime))
            throw new RuntimeException(
                    StrUtil.format("当前不在所属年度的可分配期: [{}]-[{}],不允许进行绩点分配,具体情况请联系管理员",
                            gpaAssignableBeginDateTime,
                            gpaAssignableEndDateTime)
            );
        // 校验:不允许同一项目同一个人被分配两次
        tcpPointAssignRecordDTOS.stream()
                .collect(Collectors.groupingBy(TcpPointAssignRecordDTO::assignee))
                .values()
                .forEach(group -> {
                    if (group.size() > 1)
                        throw new ServiceException("同一项目同一个人只能被分配一次绩点");
                });
        // 校验:传入的绩点分配情况总和是否与项目审核通过后的总绩点相等
        var tcpAuditPassedTotalGPA = teachConPrjVO.assignableTotalGradePoints();
        if (tcpAuditPassedTotalGPA == null) {
            log.error("严重错误!管理员审核通过项目后未更新项目可分配总绩点!?项目id:[{}]", teachConPrjVO.tcpId());
            throw new RuntimeException("系统内部异常,请联系管理员");
        }
        var newTcpAssignedTotalGPABD = BigDecimal.ZERO;
        var newTcpAssignedTotalGPA = new AtomicReference<>(BigDecimal.ZERO);
        tcpPointAssignRecordDTOS.forEach(tcpPointAssignRecordDTO -> {
            var point = tcpPointAssignRecordDTO.point();
            newTcpAssignedTotalGPA.getAndUpdate(it -> it.add(point));
        });
        newTcpAssignedTotalGPABD = newTcpAssignedTotalGPA.get();

        // 校验:判断可分配总绩点于传入分配总绩点是否相等(目前业务上未作要求,需一次性分配完)
        if (tcpAuditPassedTotalGPA.compareTo(BigDecimal.ZERO) == 0)
            throw new ServiceException("项目可分配总绩点为0,具体情况请联系管理员");
        if (tcpAuditPassedTotalGPA.compareTo(newTcpAssignedTotalGPABD) != 0) {
            throw new ServiceException("项目可分配总绩点与传入分配总绩点不相等,请确认绩点分配情况!"
                    + StrUtil.format("可分配总绩点为:[{}]," +
                            "传入的分配总绩点为:[{}]",
                    tcpAuditPassedTotalGPA,
                    newTcpAssignedTotalGPABD));
        }
        // 先删除原先的分配绩点记录
        dslContext.deleteFrom(TBL_TCP_POINT_ASSIGN_RECORD)
                .where(TBL_TCP_POINT_ASSIGN_RECORD.TCP_ID.eq(tcpId))
                .execute();

        // 批量插入新的项目绩点分配记录
        var currentUserId = SecurityUtils.getUserId();
        var tcpPointAssignRecordList = tcpPointAssignRecordDTOS
                .stream()
                .map(tcpPointAssignRecordDTO -> {
                    var tcpPointAssignRecordRecord = new TcpPointAssignRecordRecord();
                    tcpPointAssignRecordRecord.setTcpId(tcpId);
                    tcpPointAssignRecordRecord.setPoint(tcpPointAssignRecordDTO.point());
                    tcpPointAssignRecordRecord.setAssignee(tcpPointAssignRecordDTO.assignee());
                    tcpPointAssignRecordRecord.setAssigner(currentUserId);
                    tcpPointAssignRecordRecord.setAssignDateTime(LocalDateTime.now());
                    tcpPointAssignRecordRecord.setRemark(tcpPointAssignRecordDTO.remark());
                    return tcpPointAssignRecordRecord;
                })
                .toList();
        dslContext.batchInsert(tcpPointAssignRecordList).execute();

    }

    @Override
    public List<TcpPointAssignRecordVO> listTcpPointAssignRecords(ULong tcpId) {
        return tcpPointAssignRecordDAO.listTcpPointAssignRecords(tcpId);
    }

    @Override
    public Map<ULong, List<TcpAuditRecordVO>> listTcpAuditRecordGroupVOs(ULong tcpId) {
        var teachConPrj = teachConPrjDAO.findById(tcpId);
        if (teachConPrj == null || teachConPrj.getIsDeleted())
            throw new ServiceException("重点建设项目不存在或已被删除");
        // 待申报状态直接返回空集合
        if (teachConPrj.getProjectAuditStatusDict()
                .equals(dictionaryManager.getIdByEnum(DictDataEnum.TO_BE_DECLARED)))
            return MapUtil.newHashMap();

        return tcpAuditRecordDAO.listTcpAuditRecordGroupVOs(tcpId);
    }

    @Override
    public List<UserSimpleVO> listProjectLeaders(ULong tcpId) {
        var teachConPrj = teachConPrjDAO.findById(tcpId);
        if (teachConPrj == null || teachConPrj.getIsDeleted())
            throw new ServiceException("重点建设项目不存在或已被删除");
        var projectLeadersJsonArrStr = teachConPrj.getProjectLeaders().data();
        List<UserSimpleVO> projectLeaders = CollUtil.newArrayList();
        if (StrUtil.isNotBlank(projectLeadersJsonArrStr)) {
            var projectLeaderUserIds = JSONUtil
                    .toList(projectLeadersJsonArrStr, Long.class);
            projectLeaders = dslContext
                    .with(DictDataDAO.dictDataSimpleWithExpression)
                    .select(UserDAO.USER_SIMPLE_VO_SELECT.getSelect())
                    .from(TBL_USER)
                    .where(TBL_USER.USER_ID.in(projectLeaderUserIds))
                    .fetchInto(UserSimpleVO.class);
        }
        return projectLeaders;
    }

    @Override
    public void exportCollegeTeachConPrj(ULong collegeId,
                                         ULong tcpDbycId,
                                         HttpServletResponse response) {
        var teachConPrjForList = TeachConPrjDTO
                .builder()
                .collegeId(collegeId)
                .tcpDbycId(tcpDbycId)
                .tcpAuditStatusDictId(dictionaryManager.getIdByEnum(DictDataEnum.AUDIT_APPROVED))
                .build();

        var collegeName = collegeDAO.fetchOneByCollegeId(collegeId)
                .getCollegeName();
        var belongYear = belongYearConfigDAO.fetchOneByTcpDbycId(tcpDbycId)
                .getBelongYear();

        var teachConPrjVOS = teachConPrjDAO.list(teachConPrjForList);
        if (CollUtil.isEmpty(teachConPrjVOS)) {
            throw new ServiceException(
                    StrUtil.format(
                            "{}部门,{}年度没有对应教研绩点数据",
                            collegeName,
                            belongYear)
            );
        }

        // 以模板类型给重点建设项目分组
        Map<TeachConPrjTmplSimpleVO, List<TeachConPrjVO>> tcpWithTemplateTypeGroupMapList =
                teachConPrjVOS.stream()
                        .collect(Collectors.groupingBy(TeachConPrjVO::projectTemplate));
        //创建工作薄对象
        final HSSFWorkbook workbook = new HSSFWorkbook();
        //创建工作表的行
        tcpWithTemplateTypeGroupMapList.forEach((tmpl, tcpList) -> {
            BigDecimal pointSumBD = BigDecimal.ZERO;
            //创建工作表对象
            String tmplName = tmpl.tmplName();
            String sheetName = belongYear + "年度" + tmplName + "统计表";
            HSSFSheet sheet = workbook.createSheet(sheetName);
            // 设置列宽
            sheet.setDefaultColumnWidth(20);

            //----------------标题样式---------------------
            HSSFCellStyle titleStyle = workbook.createCellStyle();        //标题样式
            titleStyle.setAlignment(HorizontalAlignment.CENTER);
            titleStyle.setVerticalAlignment(VerticalAlignment.CENTER);
            Font ztFont = workbook.createFont();
            ztFont.setItalic(false);                     // 设置字体为斜体字
            ztFont.setColor(HSSFColor.HSSFColorPredefined.BLACK.getIndex());            // 将字体设置为“红色”
            ztFont.setFontHeightInPoints((short) 16);    // 将字体大小设置为18px
            ztFont.setFontName("宋体");             // 将“宋体”字体应用到当前单元格上
            ztFont.setBold(Boolean.TRUE);    //加粗
            titleStyle.setFont(ztFont);

            HSSFRow titleRow = sheet.createRow(0);
            HSSFCell titleCell = titleRow.createCell(0);
            titleCell.setCellValue(sheetName);
            titleCell.setCellStyle(titleStyle);

            // 合并单元格CellRangeAddress构造参数依次表示起始行，截至行，起始列， 截至列
            sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 7));

            HSSFRow headRow = sheet.createRow(1);
            headRow.createCell(0).setCellValue("院（部）名称");
            headRow.createCell(1).setCellValue("项目名称");
            headRow.createCell(2).setCellValue("获奖详情");
            List<Map<String, String>> tmplAttrDefListMap = tmpl.tmplAttrDef();
            for (int i = 0; i < tmplAttrDefListMap.size(); i++) {
                Map<String, String> tmplAttrDef = tmplAttrDefListMap.get(i);
                String defAttrLabel = tmplAttrDef.get("defAttrLabel");
                headRow.createCell(3 + i).setCellValue(defAttrLabel);
            }
            headRow.createCell(3 + tmplAttrDefListMap.size()).setCellValue("教师姓名");
            headRow.createCell(3 + tmplAttrDefListMap.size() + 1).setCellValue("绩点");
            for (int i = 0; i < tcpList.size(); i++) {
                TeachConPrjVO teachConPrjVO = tcpList.get(i);
                HSSFRow dataRow = sheet.createRow(i + 2);
                dataRow.createCell(0).setCellValue(teachConPrjVO.college().collegeName());
                dataRow.createCell(1).setCellValue(teachConPrjVO.projectName());
                dataRow.createCell(2).setCellValue(teachConPrjVO.projectAwardDetails());
                for (int j = 0; j < tmplAttrDefListMap.size(); j++) {
                    Map<String, String> tmplAttrDef = tmplAttrDefListMap.get(j);
                    String defAttrName = tmplAttrDef.get("defAttrName");
                    Map<String, String> tmplAttrValueMap = teachConPrjVO.tmplAttrValueMap();
                    dataRow.createCell(3 + j).setCellValue(tmplAttrValueMap.get(defAttrName));
                }
                dataRow.createCell(3 + tmplAttrDefListMap.size())
                        .setCellValue(teachConPrjVO.declarer().realName());
                BigDecimal assignableTotalGradePointsBD =
                        teachConPrjVO.assignableTotalGradePoints() != null ?
                                teachConPrjVO.assignableTotalGradePoints() :
                                BigDecimal.ZERO;
                dataRow.createCell(3 + tmplAttrDefListMap.size() + 1)
                        .setCellValue(assignableTotalGradePointsBD.toPlainString());
                pointSumBD = pointSumBD.add(assignableTotalGradePointsBD);
            }

            // 隔开两行,显示绩点合计
            HSSFRow sumRow = sheet.createRow(tcpList.size() + 1 + 2);
            sumRow.createCell(0).setCellValue("合计:");
            sumRow.createCell(headRow.getLastCellNum() - 1)
                    .setCellValue(pointSumBD.toPlainString());
        });

        //文档输出
        OutputStream out;
        try {
            String filename = belongYear + "年度" + collegeName + "学院" + "教研绩点汇总表";
            //实现文件下载保存
            response.setHeader("content-disposition", "attachment;filename="
                    + URLEncoder.encode(filename, StandardCharsets.UTF_8) + ".xls");
            out = response.getOutputStream();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            workbook.write(baos);
            byte[] xlsBytes = baos.toByteArray();
            out.write(xlsBytes);
            out.close();
        } catch (IOException e) {
            try {
                log.error("学院教研绩点汇总表导出异常: {}",
                        objectMapper.writeValueAsString(e.getMessage()));
            } catch (JsonProcessingException ex) {
                log.error("学院教研绩点汇总表导出信息序列化异常");
            }
            throw new ServiceException("内部异常,请联系管理员");
        }
    }

    @Override
    public List<PointExcelVO> getPointExcelVOList(ULong tcpDbycId) {
        return teachConPrjDAO.getPointExcelVOList(tcpDbycId);
    }


    @SneakyThrows
    private TeachConPrj fromDtoToPojo(TeachConPrj teachConPrjDb,
                                      TeachConPrjDTO teachConPrjDTO,
                                      Boolean isSendBack) {
        TeachConPrj teachConPrj;
        // 创建时新建项目对象,更新时使用数据库的项目信息
        teachConPrj = Objects.requireNonNullElseGet(teachConPrjDb, TeachConPrj::new);

        // 退回状态更新后会变为待申报状态,应将年度归属id和申报日期时间置为NULL
        if (isSendBack) {
            teachConPrj.setTcpDbycId(null);
            teachConPrj.setDeclareDateTime(null);
        }
        teachConPrj.setTcptId(teachConPrjDTO.tcptId());
        teachConPrj.setCollegeId(SecurityUtils.getCollegeId());
        // todo 删除历史迁移下的临时字段
        // 申报人默认是当前创建的用户,如果后续申报是管理员或者别的用户再更新
        teachConPrj.setDeclarer(SecurityUtils.getUserId());
        teachConPrj.setRRid(ULong.valueOf(999));
        teachConPrj.setProjectName(teachConPrjDTO.projectName());
        teachConPrj.setProjectLevelDict(teachConPrjDTO.projectLevelDictId());
        teachConPrj.setProjectPrizeOrderDict(teachConPrjDTO.projectPrizeOrderDictId());
        teachConPrj.setProjectGpaBonusStandardDict(teachConPrjDTO.projectGpaBonusStandardDictId());
        teachConPrj.setProjectAwardDetails(teachConPrjDTO.projectAwardDetails());
        if (teachConPrjDTO.projectLeaders() != null) {
            teachConPrj.setProjectLeaders(
                    JSON.json(JSONArray.toJSONString(teachConPrjDTO.projectLeaders())));
        }
        if (teachConPrjDTO.tmplAttrValueMap() != null) {
            teachConPrj.setTmplAttrValueMapJson(
                    JSON.json(JSONObject.toJSONString(teachConPrjDTO.tmplAttrValueMap())));
        }
        teachConPrj.setProjectAuditStatusDict(
                dictionaryManager.getIdByEnum(DictDataEnum.TO_BE_DECLARED)
        );
        teachConPrj.setRemark(teachConPrjDTO.remark());
        return teachConPrj;
    }


}